<!--
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL shader precision format test.</title>
<link rel="stylesheet" href="../../resources/js-test-style.css"/>
<script src="../../js/js-test-pre.js"></script>
<script src="../../js/webgl-test-utils.js"> </script>
</head>
<body>
<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas>
<div id="description"></div>
<div id="console"></div>
<script>
"use strict";
var wtu = WebGLTestUtils;
description(document.title);
debug("Tests that WebGLShaderPrecisionFormat class and getShaderPrecisionFormat work.");
debug("");
var gl = wtu.create3DContext("canvas");

debug("");
debug("Test that getShaderPrecisionFormat throws an error with invalid parameters.");
debug("");

wtu.shouldGenerateGLError(gl, gl.INVALID_ENUM, 'gl.getShaderPrecisionFormat(gl.HIGH_INT, gl.VERTEX_SHADER)');

// -

debug("");
debug("");
debug("Test that WebGLShaderPrecisionFormat values are valid:");

// The minimum values are from OpenGL ES Shading Language spec, section 4.5.
const WEBGL1_REQUIREMENT_BY_PRECISION_TYPE = {
  LOW_FLOAT   : {rangeMin:   1, rangeMax:   1, precision:  8},
  MEDIUM_FLOAT: {rangeMin:  14, rangeMax:  14, precision: 10},
  HIGH_FLOAT  : {rangeMin:  62, rangeMax:  62, precision: 16},
  LOW_INT     : {rangeMin:   8, rangeMax:   8, precision:  0},
  MEDIUM_INT  : {rangeMin:  10, rangeMax:  10, precision:  0},
  HIGH_INT    : {rangeMin:  16, rangeMax:  16, precision:  0},
};
// For `highp float`, the ESSL3 spec erroneously states min range of -2^126
// instead of IEEE's -2^127, perhaps confused by the IEEE magnitude range
// min of 2^-126.
// The ES3 spec says IEEE floats should be [127, 127, 23], and that's what
// implementations do already.
const WEBGL2_REQUIREMENT_BY_PRECISION_TYPE = {
  LOW_FLOAT   : {rangeMin:   1, rangeMax:   1, precision:  8},
  MEDIUM_FLOAT: {rangeMin:  14, rangeMax:  14, precision: 10},
  HIGH_FLOAT  : {rangeMin: 127, rangeMax: 127, precision: 23},
  LOW_INT     : {rangeMin:   8, rangeMax:   7, precision:  0},
  MEDIUM_INT  : {rangeMin:  15, rangeMax:  14, precision:  0},
  HIGH_INT    : {rangeMin:  31, rangeMax:  30, precision:  0},
};

function validateShaderPrecisionFormats(gl) {
  let requirement_by_precision_type = WEBGL1_REQUIREMENT_BY_PRECISION_TYPE;
  if (wtu.isWebGL2(gl)) {
    requirement_by_precision_type = WEBGL2_REQUIREMENT_BY_PRECISION_TYPE;
  }
  for (const shader_type of ['VERTEX_SHADER', 'FRAGMENT_SHADER']) {
    for (const [precision_type, spec_requirement] of Object.entries(requirement_by_precision_type)) {
      const was = gl.getShaderPrecisionFormat(gl[shader_type], gl[precision_type]);
      debug('');
      debug(`gl.getShaderPrecisionFormat(${shader_type}, ${precision_type}) => ${JSON.stringify(was)}`);

      globalThis.was = was;
      shouldBeTrue('was instanceof WebGLShaderPrecisionFormat');

      if (shader_type == 'FRAGMENT_SHADER' && precision_type.startsWith('HIGH_')) {
        if (was.rangeMin == 0 && was.rangeMax == 0 && was.precision == 0) {
          debug(`    Not supported. (valid)`);
          return;
        }
      }

      assertMsg(was.rangeMin >= spec_requirement.rangeMin, `was.rangeMin (${was.rangeMin}) >= spec_requirement.rangeMin (${spec_requirement.rangeMin})`);
      assertMsg(was.rangeMax >= spec_requirement.rangeMax, `was.precision (${was.rangeMax}) >= spec_requirement.rangeMax (${spec_requirement.rangeMax})`);

      if (spec_requirement.precision == 0) {
        assertMsg(was.precision == 0, `was.precision (${was.precision}) == 0`);
      } else {
        assertMsg(was.precision >= spec_requirement.precision, `was.precision (${was.precision}) >= spec_requirement.precision (${spec_requirement.precision})`);
      }
    }
  }
}

validateShaderPrecisionFormats(gl);

// -

debug("");
debug("");
debug("Test that getShaderPrecisionFormat returns the same thing every call.");
debug("");

var shaderPrecisionFormat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT);
var shaderPrecisionFormat2 = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT);
shouldBeTrue('shaderPrecisionFormat.rangeMin == shaderPrecisionFormat2.rangeMin');
shouldBeTrue('shaderPrecisionFormat.rangeMax == shaderPrecisionFormat2.rangeMax');
shouldBeTrue('shaderPrecisionFormat.precision == shaderPrecisionFormat2.precision');

debug("");
debug("");
debug("Test that specified precision matches rendering results");
debug("");

function testRenderPrecisionSetup(gl, shaderPair) {
  const program = wtu.setupProgram(gl, shaderPair);

  // Create a buffer and setup an attribute.
  // We wouldn't need this except for a bug in Safari and arguably
  // this should be removed from the test but we can't test the test itself
  // without until the bug is fixed.
  // see https://bugs.webkit.org/show_bug.cgi?id=197592
  {
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, 1, gl.STATIC_DRAW);
    const loc = gl.getAttribLocation(program, 'position');
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 1, gl.UNSIGNED_BYTE, false, 0, 0);
  }

  gl.useProgram(program);

  return program;
}

function testRenderPrecision(gl, shaderType, type, precision, expected, msg) {
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.POINTS, 0, 1);

  const pixel = new Uint8Array(4);
  gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);

  wtu.checkCanvasRect(gl, 0, 0, 1, 1, expected, msg, 5);
}

function testRenderPrecisionFloat(gl, precisionEnum, precision) {
  function test(gl, shaderPair, shaderType) {
    const format = gl.getShaderPrecisionFormat(shaderType, precisionEnum);
    const value = 2 ** format.precision - 1;

    const length = v => Math.sqrt(v.reduce((sum, v) => sum + v * v, 0));
    const normalize = (v) => {
      const l = length(v);
      return v.map(v => v / l);
    };

    const input = [Math.sqrt(value), Math.sqrt(value), Math.sqrt(value)];
    const expected = [...normalize(input).map(v => (v * 0.5 + 0.5) * 255 | 0), 255];

    const msg = `${wtu.glEnumToString(gl, shaderType)}: ${precision} float precision: ${format.precision}, rangeMin: ${format.rangeMin}, rangeMax: ${format.rangeMax}`;
    const program = testRenderPrecisionSetup(gl, shaderPair);
    const vLocation = gl.getUniformLocation(program, 'v');
    gl.uniform3fv(vLocation, input);
    testRenderPrecision(gl, shaderType, 'float', precision, expected, msg);
  }

  {
    const vs = `
    attribute vec4 position;
    uniform ${precision} vec3 v;
    varying ${precision} vec4 v_result;
    void main() {
      gl_Position = position;
      gl_PointSize = 1.0;
      v_result = vec4(normalize(v) * 0.5 + 0.5, 1);
    }
    `;

    const fs = `
    precision ${precision} float;
    varying ${precision} vec4 v_result;
    void main() {
      gl_FragColor = v_result;
    }
    `;

    test(gl, [vs, fs], gl.VERTEX_SHADER);
  }

  {
    const vs = `
    attribute vec4 position;
    void main() {
      gl_Position = position;
      gl_PointSize = 1.0;
    }
    `;

    const fs = `
    precision ${precision} float;
    uniform ${precision} vec3 v;
    void main() {
      gl_FragColor = vec4(normalize(v) * 0.5 + 0.5, 1);
    }
    `;

    test(gl, [vs, fs], gl.FRAGMENT_SHADER);
  }
}

function testRenderPrecisionInt(gl, precisionEnum, precision) {
  function test(gl, shaderPair, shaderType) {
    const format = gl.getShaderPrecisionFormat(shaderType, precisionEnum);
    const value = 1 << (format.rangeMax - 1);

    const input = [value, value, value, value];
    const expected = [255, 255, 255, 255];

    const msg = `${wtu.glEnumToString(gl, shaderType)}: ${precision} int precision: ${format.precision}, rangeMin: ${format.rangeMin}, rangeMax: ${format.rangeMax}`;
    const program = testRenderPrecisionSetup(gl, shaderPair);
    gl.uniform1i(gl.getUniformLocation(program, 'v'), value);
    gl.uniform1f(gl.getUniformLocation(program, 'f'), value);
    testRenderPrecision(gl, shaderType, 'int', precision, expected, msg);
  }

  // Only use highp in the fragment shader if caller has determined it's supported.
  const uniformPrecision = (precision == 'highp' ? 'highp' : 'mediump');

  {
    const vs = `
    attribute vec4 position;
    uniform ${precision} int v;
    uniform highp float f;
    varying vec4 v_result;

    void main() {
      gl_Position = position;
      gl_PointSize = 1.0;
      float diff = abs(float(v) - f);
      bool pass = diff < 1.0;
      v_result = vec4(pass);
    }
    `;

    const fs = `
    precision mediump float;
    varying vec4 v_result;
    void main() {
      gl_FragColor = v_result;
    }
    `;
    test(gl, [vs, fs], gl.VERTEX_SHADER);
  }

  {
    const vs = `
    attribute vec4 position;
    void main() {
      gl_Position = position;
      gl_PointSize = 1.0;
    }
    `;

    const fs = `
    precision ${precision} float;
    uniform ${precision} int v;
    uniform ${uniformPrecision} float f;

    void main() {
      mediump float diff = abs(float(v) - f);
      bool pass = diff < 1.0;
      gl_FragColor = vec4(pass);
    }
    `;

    test(gl, [vs, fs], gl.FRAGMENT_SHADER);
  }
}

// because the canvas can be 16 bit IIRC
const fb = gl.createFramebuffer(gl.FRAMEBUFFER);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);

const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

gl.framebufferTexture2D(
    gl.FRAMEBUFFER,
    gl.COLOR_ATTACHMENT0,
    gl.TEXTURE_2D,
    tex,
    0);

gl.viewport(0, 0, 1, 1);

{
  testRenderPrecisionFloat(gl, gl.LOW_FLOAT, 'lowp');
  testRenderPrecisionFloat(gl, gl.MEDIUM_FLOAT, 'mediump');

  const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
  if (format.precision !== 0) {
    testRenderPrecisionFloat(gl, gl.HIGH_FLOAT, 'highp');
  }
}

{
  testRenderPrecisionInt(gl, gl.LOW_INT, 'lowp');
  testRenderPrecisionInt(gl, gl.MEDIUM_INT, 'mediump');

  const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT);
  if (format.rangeMax !== 0) {
    testRenderPrecisionInt(gl, gl.HIGH_INT, 'highp');
  }
}

finishTest();
</script>

</body>
</html>


